package com.db.stockexchange.server.connection;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Logger;

import com.db.stockexchange.common.messages.FIXMessage;
import com.db.stockexchange.common.messages.client.CancelRequestMessage;
import com.db.stockexchange.common.messages.client.LogonMessage;
import com.db.stockexchange.common.messages.client.LogoutMessage;
import com.db.stockexchange.common.messages.client.NewOrderMessage;
import com.db.stockexchange.common.messages.server.AckMessage;
import com.db.stockexchange.common.messages.server.ExecutionRejectMessage;
import com.db.stockexchange.common.messages.server.RejectedMessage;
import com.db.stockexchange.common.parsing.FIXParseException;
import com.db.stockexchange.common.parsing.FIXParser;
import com.db.stockexchange.common.parsing.RejectReason;

/**
 * 
 * @author Rayman
 * Represents connection between client and server
 */
public class Connection implements Runnable {
	static private final String IN = " IN";
	static private final String OUT = "OUT";
	static private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
	
	private String clientID = null;
	private Socket socket;
	private BufferedReader reader;
	private PrintWriter writer;
	private FIXParser parser;
	private ConnectionPool pool;
	private Logger logger;
	
	BlockingQueue<FIXMessage> toEngineQueue;
	
	public Connection(Socket socket, BlockingQueue<FIXMessage> toEngineQueue, ConnectionPool pool) throws IOException {
		try {
			this.socket = socket;
			reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			writer = new PrintWriter(socket.getOutputStream(), true);
			parser = new FIXParser();
			this.toEngineQueue = toEngineQueue;
			this.pool = pool;
		} catch (IOException e) {
			this.close();
			throw e;
		}
		this.logger = Logger.getLogger(this.getClass().getName(), null);
	}

	@Override
	public void run() {
		while_loop:
		while (true) {
			try {
				String s = reader.readLine();
				if (s != null && !s.isEmpty()) {
					FIXMessage message = parser.parseMessage(s);

					logger.info(String.format("%s: %s: %s", IN, dateFormat.format(new Date()), message));
					
					switch (message.getType()) {
					case LOGON:
						logon((LogonMessage)message);
						break;
					case LOGOUT:
						logout((LogoutMessage)message);
						break while_loop;
					case NEW_ORDER:
						newOrder((NewOrderMessage)message);
						break;
					case CANCEL_ORDER:
						cancelRequest((CancelRequestMessage)message);
						break;
					default:
						logger.warning(String.format("Received message has unsupported type <%s>", message.getType()));
					}
				}
			} catch (SocketException e) {
				break while_loop;
			}
			catch (IOException e) {
				if (socket.isClosed())
					break while_loop;
				logger.warning(e.getClass() + " occurred while reading client message: " + e.getMessage());
			} catch (FIXParseException e) {
				logger.warning("FIXParesException occurred while parsing received message: " + e.getRejectedMessage().getReasonDetail());
				sendMessage(e.getRejectedMessage());
			}
		}
		
		close();
	}

	private void logon(LogonMessage message) {
		clientID = message.getSenderCompID();
		boolean res = pool.add(this);
		if (res) {
			logger.fine(String.format("Logon is successful for client <%s>", clientID));
			sendMessage(message);
		} else {
			logger.warning(String.format("Logon is unsuccesful. Client with name <%s> already exists", clientID));
			sendMessage(new RejectedMessage(RejectReason.COMP_ID_PROBLEM.asNumber(), 
					String.format("Client with name <%s> already exists", clientID)));
		}
	}
	
	private void logout(LogoutMessage message) {
		if (clientID == null) {
			logger.warning(String.format("Logout is unsuccesful. Client with name <%s> is not logged on", clientID));
			sendMessage(new RejectedMessage(RejectReason.COMP_ID_PROBLEM.asNumber(), "You are not logged on. Please logon first."));
		} else if (!clientID.equals(message.getSenderCompID())) {
			logger.warning(String.format("Logout is unsuccesful. Client name <%s> is not same as received <%s>", clientID, message.getSenderCompID()));
			sendMessage(new RejectedMessage(RejectReason.COMP_ID_PROBLEM.asNumber(), "Sender Company ID is not equals to logon Sender Company ID"));
		} else {
			pool.remove(this);
			logger.fine(String.format("Logout is successful for client <%s>", clientID));
		}
	}

	private void newOrder(NewOrderMessage message) {
		
		logger.fine(String.format("New order with id <%s> received from client <%s>", message.getOrderID(), message.getSenderCompID()));
		
		if (clientID == null) {
			logger.warning(String.format("Logout is unsuccesful. Client with name <%s> is not logged on", clientID));
			sendMessage(new ExecutionRejectMessage(message.getSenderCompID(), message.getOrderID(), 
					message.getSymbol(), message.getSide(), "You are not logged on. Please logon first."));
		} else {
			sendMessage(new AckMessage(message.getSenderCompID(), message.getOrderID(), 
					message.getSymbol(), message.getSide(), message.getOrderQuantity(), 
					0, message.getOrderQuantity(), message.getPrice()));
			
			try {
				toEngineQueue.put(message);
			} catch (InterruptedException e) {}
		}
	}
	
	private void cancelRequest(CancelRequestMessage message) {
		if (clientID == null) {
			logger.warning(String.format("Logout is unsuccesful. Client with name <%s> is not logged on", clientID));
			sendMessage(new ExecutionRejectMessage(message.getSenderCompID(), message.getOrderID(), 
					message.getSymbol(), message.getSide(), "You are not logged on. Please login first."));
		} else {
			try {
				toEngineQueue.put(message);
			} catch (InterruptedException e) {}
		}
	}
	
	public void sendMessage(final FIXMessage message) {
		logger.info(String.format("%s: %s: %s", OUT, dateFormat.format(new Date()), message));
		writer.println(parser.representMessage(message));
	}
	
	public void close() {
		try {
			if (socket != null)
				socket.close();
			if (reader != null)
				reader.close();
			if (writer != null)
				writer.close();			
		} catch (IOException e) {
			logger.warning(String.format("Exception occured while closing connection for client <%s>: <%s>", clientID, e.getMessage()));
		}
		
		pool.remove(this);
	}

	public String getClientID() {
		return clientID;
	}
}
